Reference user-defined types by id - #1966
Closed
leighmcculloch wants to merge 8 commits into
Closed
Conversation
This was referenced Jul 29, 2026
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
Stacked on #1965, so the diff here is only the id linking. Depends on unmerged and unreleased changes in:
A valid merge order for the whole stack:
SCSpecTypeUDTV2to the contract spec XDRRefvariants of the generated typesReftypesSCSpecTypeUDTV2What
Emit every reference to a user-defined type as
ScSpecTypeUdtv2, which identifies the referenced type by an 8-byte id and nothing else, where before it was aScSpecTypeUdtcarrying only a name. Each user-defined type gains aspec_type_id()const fn returning its own id — the truncated SHA256 of its spec entry, over a canonical form with all contained ids zeroed — and a reference calls it on the referenced type, so the derive macro on a struct never has to know the id of a type it merely mentions.Both the encoding and the hashing behind
spec_type_id()happen at const evaluation time. Each type also renders its spec entry in canonical form as__SPEC_XDR_CANONICAL_REF, andspec_type_id()hashes that entry's const-encoded XDR. The id is therefore derived from the very bytes the contract emits, rather than from a second encoding of them made in the macro.sha2has no const API, so the hashing uses thesha2-constcrate, added as a dependency ofsoroban-sdkand re-exported for macro-generated code to reach.Client generation in
soroban-spec-rustresolves a reference's id back to a name, by matching it against the id of each user-defined type the spec defines, in a pass that runs before generation. A reference whose id matches no definition in the spec is an error rather than a silently wrong name.Why
A spec's references were linked to their definitions by name alone, which is not enough to check that the two agree: a consumer reading
UdtStructin a function signature cannot tell whether it is theUdtStructit holds a definition for, and a definition that changed shape while keeping its name looks identical to one that did not. An id derived from the referenced type's own definition makes that link verifiable. Carrying the id instead of the name, rather than alongside it, leaves one thing identifying the referenced type instead of two that can disagree.Calling
spec_type_id()on the referenced type rather than computing the id in the macro is what makes this work at all: the macro expandingstruct A { b: B }sees only the tokensB, notB's definition, and it may live in another crate, so the id can only be resolved where both are visible, which is const evaluation. Zeroing contained ids in the hashed form keeps a type's identity independent of the ids it references, which is what makes it well-defined for types that reference each other or themselves — and at const evaluation time that zeroing also keeps a self- or mutually-recursive type from forming an evaluation cycle.spec_type_id()is emitted for every user-defined type, including one whose spec export is disabled, because a reference from elsewhere still needs the id. Rendering a reference descends the Rust type in step with the spec type it was mapped from, so the call is emitted on the type as written, path qualification and all. Spec shaking's markers hash the same canonical form, so a marker computed in the macro before ids are resolved still matches the entry read back out of a built wasm.Known limitations
The id is truncated to 8 bytes, so it identifies a type by content only to the extent 64 bits allow. Nothing consumes the id yet beyond resolving it back to a name; this only produces it.
Because a reference no longer carries a name, the 60-character limit that the reference's name field used to impose is now checked directly against the limit on the name of the type's own definition entry, which is where the limit actually lives.
Rendering each entry twice, once exported and once canonical, roughly doubles the generated spec tokens for a type. The extra entry is const-evaluated and unused at runtime, so it costs nothing in a built contract: rebuilding all 43 test wasms over this change leaves every
contractspecv0section byte-identical, and 42 of the 43 wasms byte-identical outright.